第15章 初始化

整个初始化过程相当烦琐,要完成诸如命令行参数整理、环境变量设置,以及内存分配器、垃圾回收器和并发调度器的工作现场准备。

依照第14章找出的线索,先依次看看几个初始化函数的内容。依旧用设置断点命令确定函数所在的源文件名和代码行号。

(gdb)b runtime.args Breakpoint 7 at 0x42ebf0:file/usr/local/go/src/runtime/runtime1.go,line 48.

(gdb)b runtime.osinit Breakpoint 8 at 0x41e9d0:file/usr/local/go/src/runtime/os1_linux.go,line 172.

(gdb)b runtime.schedinit Breakpoint 9 at 0x424590:file/usr/local/go/src/runtime/proc1.go,line 40.

函数args整理命令行参数,这个没什么需要深究的。

runtime1.go

func args(c int32,v**byte) { argc=c argv=v sysargs(c,v) }

函数osinit确定CPU Core数量。

os1_linux.go

func osinit() { ncpu=getproccount() }

最关键的就是schedinit这里,几乎我们要关注的所有运行时环境初始化构造都是在这里被调用的。函数头部的注释列举了启动过程,也就是第14章的内容,不过信息太过简洁了。

proc1.go

//The bootstrap sequence is: // // call osinit // call schedinit // make&queue new G // call runtime·mstart func schedinit() { // 最大系统线程数量限制,参考标准库runtime/debug.SetMaxThreads sched.maxmcount=10000

// 栈、内存分配器、调度器相关初始化 stackinit() mallocinit() mcommoninit(g.m)

// 处理命令行参数和环境变量 goargs() goenvs()

// 处理GODEBUG、GOTRACEBACK调试相关的环境变量设置 parsedebugvars()

// 垃圾回收器初始化 gcinit()

// 通过CPU Core和GOMAXPROCS环境变量确定P数量 procs:=int(ncpu) if n:=atoi(gogetenv(“GOMAXPROCS”));n>0{ if n> _MaxGomaxprocs{ n= _MaxGomaxprocs } procs=n }

// 调整P数量 if procresize(int32(procs)) !=nil{ throw(“unknown runnable goroutine during bootstrap”) } }

内存分配器、垃圾回收器、并发调度器的初始化细节需要涉及很多专属特征,先不去理会,留待后续章节再做详解。

事实上,初始化操作到此并未结束,因为接下来要执行的是runtime.main,而不是用户逻辑入口函数main.main。

(gdb)b runtime.main Breakpoint 10 at 0x423250:file/usr/local/go/src/runtime/proc.go,line 28.

在这里我们关注的焦点是:包初始化函数init的执行。

proc.go

//The main goroutine. func main() { // 执行栈的最大限制:1 GB on 64-bit,250 MB on 32-bit. if ptrSize==8{ maxstacksize=1000000000 }else{ maxstacksize=250000000 }

// 启动系统后台监控(定期垃圾回收,以及并发任务调度相关的信息) systemstack(func() { newm(sysmon,nil) })

// 执行runtime包内所有初始化函数init runtime_init()

// 启动垃圾回收器后台操作 gcenable()

// 执行所有的用户包(包括标准库)初始化函数init main_init()

// 执行用户逻辑入口main.main函数 main_main()

// 执行结束,返回退出状态码 exit(0) }

与之相关的就是runtime_init和main_init这两个函数,它们都由编译器动态生成。

proc.go

//go:linkname runtime_init runtime.init func runtime_init()

//go:linkname main_init main.init func main_init()

//go:linkname main_main main.main func main_main()

注意链接后符号名的变化:runtime_init > runtime.init。

我们准备一个稍微复杂点的示例,看看编译器究竟干了什么。

  • | +-main.go,test.go | +- | +-sum.go

lib/sum.go

package lib

func init() { println(“sum.init”) }

func Sum(x…int)int{ n:=0 for_,i:=range x{ n+=i }

return n }

test.go

package main

import( “lib” )

func init() { println(“test.init”) }

func test() { println(lib.Sum(1,2,3)) }

main.go

package main

import( _ “net/http” // 引入一个标准库里的包 )

func init() { println(“main.init.2”) }

func main() { test() }

func init() { println(“main.init.1”) }

编译,执行输出:

$go build-gcflags”-N-l” -o test

$ ./test sum.init main.init.2 main.init.1 test.init 6

接下来我们用反汇编工具,看看最终动态生成代码的真实面目。

$go tool objdump-s”runtime.init\b”test

TEXT runtime.init.1(SB) /usr/local/go/src/runtime/alg.go alg.go:322 …

TEXT runtime.init.2(SB) /usr/local/go/src/runtime/mstats.go mstats.go:148 …

TEXT runtime.init.3(SB) /usr/local/go/src/runtime/panic.go panic.go:154 …

TEXT runtime.init.4(SB) /usr/local/go/src/runtime/proc.go proc.go:140 …

TEXT runtime.init(SB) /usr/local/go/src/runtime/zversion.go zversion.go:9 … panic.go:9 … select.go:45 …

zversion.go:9 CALL runtime.init.1(SB) zversion.go:9 CALL runtime.init.2(SB) zversion.go:9 CALL runtime.init.3(SB) zversion.go:9 CALL runtime.init.4(SB) zversion.go:9 MOVL0x58,SP zversion.go:9 RET

命令行工具go tool objdump可用来查看实际生成的汇编代码,参数使用正则表达式。当然如果习惯Intel格式,那么还是用GDB吧。

很显然,runtime内相关的多个init函数被赋予唯一符号名,然后再由runtime.init进行统一调用。注意,zversion.go也是动态生成的。

zversion.go

//auto generated by go tool dist

package runtime

const defaultGoroot= /usr/local/go const theVersion= go1.5.1 const goexperiment= “ const stackGuardMultiplier=1 var buildVersion=theVersion

至于main.init,情况基本一致。区别在于它负责调用非runtime包的初始化函数。

$go tool objdump-s”main.init\b”test

TEXT main.init.1(SB)src/main.go main.go:7 …

TEXT main.init.2(SB)src/main.go main.go:15 …

TEXT main.init.3(SB)src/test.go test.go:7 …

TEXT main.init(SB)src/test.go test.go:13 … test.go:13 CALL net/http.init(SB) test.go:13 CALL test/lib.init(SB) test.go:13 CALL main.init.1(SB) test.go:13 CALL main.init.2(SB) test.go:13 CALL main.init.3(SB) test.go:13 MOVL$0x2,0x48d543(IP) test.go:13 RET

被引用的包,包括lib和标准库net/http里的init函数,都被main.init调用。

虽然从当前版本的编译器角度来说,init的执行顺序和依赖关系、文件名,以及定义顺序有关。但这种次序非常不便于维护和理解,极易造成潜在错误,所以强烈建议让init只做该做的事情:局部初始化。

最后需要记住:

  • 所有init函数都在同一个goroutine内执行。
  • 所有init函数结束后才会执行main.main函数。